﻿#include"server.h"
#include<arpa/inet.h>//或arpa/socket.h
#include<sys/epoll.h>//epoll头文件
#include<stdio.h>//NULL 定义
#include<fcntl.h>
#include<errno.h>
#include<string.h>
#include<strings.h>
#include<sys/stat.h>
#include<assert.h>
#include<sys/sendfile.h>
#include<dirent.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>
#include<ctype.h>
struct FdInfo {
	int fd;
	int epfd;
	pthread_t tid;
};

int initListenFD(unsigned short port) {
	//1、 创建监听的fd（文件描述符）
	int lfd = socket(AF_INET, SOCK_STREAM, 0);//使用流式传输，默认0设置TCP协议
	if (lfd == -1) {
		perror("socket");
		return -1;
	}

	//2、 设置端口地址复用
	int opt = 1;
	//设置套接字选项 level级别：SOL_SOCKET ；optname选项名：SO_REUSEADDR（重用本地地址）；opt 标志（是否选择）--详见 UNIX_API ——p175
	int ret = setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
	if (ret == -1) {
		perror("setsockopt");
		return -1;
	}
	//3、绑定地址
	struct sockaddr_in addr;
	addr.sin_family = AF_INET;
	addr.sin_port = htons(port);
	addr.sin_addr.s_addr = INADDR_ANY;//或inet_pton(AF_INET,"192...",&addr.sin_addr.s_addr)
	ret = bind(lfd, (struct sockaddr*)&addr, sizeof(addr));
	if (ret == -1) {
		perror("bind");
		return -1;
	}

	//4、设置监听
	ret = listen(lfd, 128);//监听数最大128
	if (ret == -1)
	{
		perror("listen");
		return -1;
	}

	return lfd;
}

int epollrun(int lfd)
{
	printf("开始epollrun\n");
	//1、创建epoll实例
	int epfd = epoll_create(1);//参数无意义，大于0即可
	if (epfd == -1) {
		perror("epoll_create");
		return -1;
	}
	//2、 lfd上树
	struct epoll_event ev;
	ev.data.fd = lfd;
	ev.events = EPOLLIN;//水平模式
	//ev.events = EPOLLIN|EPOLLET;//边沿模式，需要设置文件描述符为非阻塞
	int ret = epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
	if (ret == -1) {
		perror("epoll_ctl");
		return -1;
	}
	//3、 检测
	struct epoll_event evs[1024];//传出参数，就绪的文件描述符的信息，从头开始存，如果装不下，下次检测的时候接着存
	while (1) {
		int num = epoll_wait(epfd, evs, sizeof(evs) / sizeof(struct epoll_event), -1);
		for (int i = 0; i < num; ++i) {
			struct FdInfo* info = (struct FdInfo*)malloc(sizeof(struct FdInfo));
			int fd = evs[i].data.fd;
			info->epfd = epfd;
			info->fd = fd;
			if (fd == lfd) {//是监听的文件描述符，建立新连接
				//acceptClient(lfd, epfd);
				pthread_create(&info->tid, NULL, acceptClient, info);
			}
			else {
				//不是监听的文件描述符,接收数据
				//recvHttpRequest(fd, epfd);
				pthread_create(&info->tid, NULL, recvHttpRequest, info);
			}
		}
	}
	return 0;
}
//int acceptClient(int lfd, int epfd)
void* acceptClient(void* arg)
{
	struct FdInfo* info = (struct FdInfo*)arg;
	printf("acceptClient threadid:%ld", info->tid);

	// 1、建立连接
	printf("建立连接\n");
	int cfd = accept(info->fd, NULL, NULL);
	if (cfd == -1) {
		perror("accpet");
		return NULL;
	}
	//2、设置非阻塞（epoll）
	int flag = fcntl(cfd, F_GETFL);
	flag |= O_NONBLOCK;//按位或
	//O_NONBLOCK=04000,有一个二进制位为1，按位或即可设置某一标志位为1
	fcntl(cfd, F_SETFL, flag);//设置操作选项

	//3、cfd 添加到epoll中

	struct epoll_event ev;
	ev.data.fd = cfd;
	ev.events = EPOLLIN | EPOLLET;
	int ret = epoll_ctl(info->epfd, EPOLL_CTL_ADD, cfd, &ev);
	if (ret == -1) {
		perror("epoll_ctl");
		return NULL;
	}
	free(info);
	return NULL;
}

//int recvHttpRequest(int cfd, int epfd)
void* recvHttpRequest(void* arg)
{
	struct FdInfo* info = (struct FdInfo*)arg;
	printf("recvHttpRequest threadid:%ld", info->tid);//pthread_self()
	printf("开始接收数据\n");
	char buf[4096] = { 0 };
	char temp[1024] = { 0 };
	//边沿非阻塞，循环读入所有数据
	int len = 0;
	int totle = 0;//已接收的数据长度
	while ((len = recv(info->fd, temp, sizeof(temp), 0)) > 0)
	{
		if (totle + len < sizeof buf) {
			memcpy(buf + totle, temp, len);
		}
		totle += len;//更新写入开始位
	}
	//判断数据是否接收完毕
	if (len == -1 && errno == EAGAIN) {//数据接收完毕
		//解析请求行
		char* pt = strstr(buf, "\r\n");//指向行结尾，pt指向\r
		int reqLen = pt - buf;//请求行的长度
		buf[reqLen] = '\0';//请求行结束后加\0 表示字符串结束
		parseRequestLine(buf, info->fd);
	}
	else if (len == 0) {
		//客户端断开了连接，从epoll中删除
		epoll_ctl(info->epfd, EPOLL_CTL_DEL, info->fd, NULL);
		close(info->fd);
	}
	else {
		perror("recv");
	}
	free(info);
	return NULL;
}

int  parseRequestLine(const char* line, int cfd) {
	//解析请求行

		//http 三个部分中间有空格间隔，读到空格就可分开数据
		//sscanf()格式化读取

	/*
	*	get /xxx/1.jpg http/1.11         服务器工作目录
	*/

	char method[12];
	char path[1024];
	sscanf(line, "%[^ ] %[^ ]", method, path);
	printf("methon:%s,path:%s\n", method, path);
	if (strcasecmp(method, "get") != 0)//判断请求
	{
		return -1;
	}
	decodeMsg(path, path);//转换请求路径，覆盖旧的数据 %E5 三位足够存放中文
	//处理客户端请求静态资源（目录或文件）
	char* file = NULL;

	if (strcmp(path, "/") == 0) {//是资源根目录
		file = "./";
	}
	else {
		//不是访问根目录

		// /xxx/1.jpg 绝对路径 留下相对路径 ./xxx/1.jpg 或 xxx/1.jpg
		file = path + 1;//略过 “/"
	}
	// 获取文件属性
	struct stat st;
	int ret = stat(file, &st);//传入路径  传出文件属性
	if (ret == -1) {
		//文件不存在返回 404
		sendHeadMsg(cfd, 404, "NOT FOUND", getFileType(".html"), -1);//设置-1 告知浏览器不知道长度，浏览器读取时自己算
		sendFile("404.html", cfd);//发送404.html
		return 0;
	}
	//判断文件类型
	if (S_ISDIR(st.st_mode)) {//是目录
		//把本地目录的内容发送给客户端
		sendHeadMsg(cfd, 200, "OK", getFileType(".html"), -1);
		sendDir(file, cfd);
	}
	else {//是文件
		//把文件的内容发送给客户端
		sendHeadMsg(cfd, 200, "OK", getFileType(file), st.st_size);
		sendFile(file, cfd);
	}
	return 0;
}
int sendFile(const char* fileName, int cfd) {
	//读一部分，发一部分（tcp保证传输完成），不用读入再发（节省内存）

	int fd = open(fileName, O_RDONLY);

	assert(fd > 0);//断言文件打开正确，否则立即弹出错误

#if 0 //宏判断注释
	while (1) {
		char buf[1024];
		int len = read(fd, buf, sizeof buf);//读入数据
		if (len > 0) {
			send(cfd, buf, sizeof buf, 0);//发送数据
			usleep(10);//客户端需要时间处理（接收，缓存，处理），发慢点
		}
		else if (len == 0) {
			break;
		}
		else {
			perror("read");
		}
	}
#else
	off_t offset = 0;
	//使用linux 系统函数 sendfile（完全在内核中完成操作，无拷贝，节省资源）
	long size = lseek(fd, 0, SEEK_END);//当前文件指针 移动到文件end  返回值=0+移动距离
	lseek(fd, 0, SEEK_SET);//文件指针返回文件起始位置

	while (offset < size) {//文件没有读完
		long ret = sendfile(cfd, fd, &offset, size - offset);//offset 指定默认读取位置,发送数据前根据偏移量读取文件，发送完后更新该偏移量
		printf("ret value: %d\n", ret);
		if (ret == -1 && errno == EAGAIN) {//cfd 已经被设置为非阻塞，正常情况阻塞模式就会阻塞，不会直接返回-1
			printf("没数据...\n");
		}
	}
#endif
	close(fd);
	return 0;
}
int sendHeadMsg(int cfd, int status, const char* descr, const char* type, int length) {
	//状态行
	char buf[4096] = { 0 };
	sprintf(buf, "http/1.1 %d %s\r\n", status, descr);
	//响应行
	sprintf(buf + strlen(buf), "content-type: %s\r\n", type);
	sprintf(buf + strlen(buf), "content-length: %d\r\n\r\n", length);//要额外加一个空行以区分状态头信息

	send(cfd, buf, strlen(buf), 0);
	return 0;
}

const char* getFileType(const char* name)
{
	// a.jpg a.mp4 a.html
	// 自右向左查找‘.’字符, 如不存在返回NULL
	const char* dot = strrchr(name, '.');
	if (dot == NULL)
		return "text/plain; charset=utf-8";	// 纯文本
	if (strcmp(dot, ".html") == 0 || strcmp(dot, ".htm") == 0)
		return "text/html; charset=utf-8";
	if (strcmp(dot, ".jpg") == 0 || strcmp(dot, ".jpeg") == 0)
		return "image/jpeg";
	if (strcmp(dot, ".gif") == 0)
		return "image/gif";
	if (strcmp(dot, ".png") == 0)
		return "image/png";
	if (strcmp(dot, ".css") == 0)
		return "text/css";
	if (strcmp(dot, ".au") == 0)
		return "audio/basic";
	if (strcmp(dot, ".wav") == 0)
		return "audio/wav";
	if (strcmp(dot, ".avi") == 0)
		return "video/x-msvideo";
	if (strcmp(dot, ".mov") == 0 || strcmp(dot, ".qt") == 0)
		return "video/quicktime";
	if (strcmp(dot, ".mpeg") == 0 || strcmp(dot, ".mpe") == 0)
		return "video/mpeg";
	if (strcmp(dot, ".vrml") == 0 || strcmp(dot, ".wrl") == 0)
		return "model/vrml";
	if (strcmp(dot, ".midi") == 0 || strcmp(dot, ".mid") == 0)
		return "audio/midi";
	if (strcmp(dot, ".mp3") == 0)
		return "audio/mpeg";
	if (strcmp(dot, ".ogg") == 0)
		return "application/ogg";
	if (strcmp(dot, ".pac") == 0)
		return "application/x-ns-proxy-autoconfig";
	return "text/plain; charset=utf-8";
}
/*
<html>							#根节点
	<head>						#头
		<title>test</title>		#标题，tab页显示
	</head>
	<body>						#起始
		<table>
			<tr>				#行头
				<td></td>		#列
				<td></td>
			</tr>				#行尾
			<tr>
				<td></td>
				<td></td>
			</tr>
		</table>
	</body>						#结束
</html>
*/
int sendDir(const char* dirName, int cfd)
{
	char buf[4096] = { 0 };
	sprintf(buf, "<html><head><title>%s</title></head><body><table>", dirName);
	struct dirent** namelist;//需要手动释放
	int num = scandir(dirName, &namelist, NULL, alphasort);//文件夹名 传出参数 筛选器 排序函数
	for (int i = 0; i < num; ++i)
	{
		//取出文件名,namelist 指向一个数组指针 struct dirent* temp[]
		char* name = namelist[i]->d_name;
		struct stat st;
		char subPath[1024] = { 0 };//获取“相对服务器工作目录”路径
		sprintf(subPath, "%s/%s", dirName, name);
		stat(subPath, &st);//获取文件名状态
		if (S_ISDIR(st.st_mode)) {//是目录
			//a 标签 跳转链接 <a href="地址/">name</a>   地址不带/ 表示访问文件
			sprintf(buf + strlen(buf),
				"<tr><td><a href=\"%s/\">%s</a></td><td>%ld</td></tr>",
				name, name, st.st_size);
		}
		else
		{//不是目录,是文件
			sprintf(buf + strlen(buf),
				"<tr><td><a href=\"%s\">%s</a></td><td>%ld</td></tr>",
				name, name, st.st_size);
		}
		send(cfd, buf, strlen(buf), 0);
		memset(buf, 0, sizeof(buf));
		free(namelist[i]);
	}
	sprintf(buf, "</table></body></html>");
	send(cfd, buf, strlen(buf), 0);
	free(namelist);
	return 0;
}

/*get 命令不支持传输中文，传输过程中会将中文转为Unicode码，并以16进制字符传输，
  读入后需要转换为原来的字符(int -> char)才能正确调用文件
*/
//解码： to 存储解码之后的数据, 传出参数, from被解码的数据, 传入参数
void decodeMsg(char* to, char* from)
{
	for (; *from != '\0'; ++to, ++from)
	{
		// isxdigit -> 判断字符是不是16进制格式, 取值在 0-f
		// Linux%E5%86%85%E6%A0%B8.jpg
		if (from[0] == '%' && isxdigit(from[1]) && isxdigit(from[2]))
		{
			// 将16进制的数 -> 十进制 将这个数值赋值给了字符 int -> char
			// B2 == 178
			// 将3个字符, 变成了一个字符, 这个字符就是原始数据
			*to = hexToDec(from[1]) * 16 + hexToDec(from[2]);

			// 跳过 from[1] 和 from[2] 因此在当前循环中已经处理过了
			from += 2;
		}
		else
		{
			// 字符拷贝, 赋值
			*to = *from;
		}
	}
	*to = '\0';
}
int hexToDec(char c)
{
	if (c >= '0' && c <= '9')
		return c - '0';
	if (c >= 'a' && c <= 'f')
		return c - 'a' + 10;
	if (c >= 'A' && c <= 'F')
		return c - 'A' + 10;

	return 0;
}